
今天用一個實際的 stack 來聊資源上限。這個叫 devstack 的環境是我研究 RAG 用的,目前跑在 Docker Compose 上。它是我第一次認真想「每個服務到底該給多少」的地方,而這些數字之後搬到 K8s,會直接變成 resources.limits。
四個主要容器,加一個 dev 資料庫:
| 容器 | 角色 | 實測用量 | 我設的上限 |
|---|---|---|---|
devstack-rag-engine |
推論引擎(Python / uvicorn) | 約 2.19 GiB | 6 GiB |
devstack-qdrant |
向量資料庫 | 很小 | 1 GiB |
devstack-leapchat |
前端 | 很小 | 512 MiB |
devstack-caddy |
反向代理 | 很小 | 128 MiB |
petbook-dev-postgres |
dev 資料庫 | 很小 | 512 MiB |
先看一個很明顯的事實:只有推論引擎是「重」的,其他四個都很小。 這跟 Day 06 講的三種型態完全對得上:引擎是型態三(常駐的大東西),其他都是型態一(常駐的小東西)。
(以下是把幾次調整整理後的示意版本)
第一版:不設。 一開始我什麼都沒設,理由很單純:不知道該設多少。結果就是 Day 04 那種狀況:推論引擎跟主機上的其他東西互搶記憶體,誰也不知道界線在哪。
第二版:憑感覺設。 推論引擎我給了 4 GiB,直覺是「模型沒那麼大,留一倍應該夠」。跑了一陣子之後,某次載入比較大的 embedding 模型,就直接 OOM 被殺。這時我學到第一課:上限要看最壞情況,不是平均情況。 推論引擎的記憶體會在「載入模型那一瞬間」衝高,平常會回落,所以上限得撐得住那個瞬間。
第三版:實測加 buffer。 用 docker stats 觀察一陣子,抓到的高點大約 2.2 GiB;再考慮之後可能換更大的模型,最後設 6 GiB。這個數字用到現在沒再動過。
向量庫跟反向代理剛好相反:實測用量穩定又很低。上限設成實測值的好幾倍,目的不是防日常爆量,而是防「意外」發生時不要拖垮別人,例如向量庫某天突然湧進一大批寫入。
調到第三版,我才真的想通「為什麼要設上限」。不是為了省記憶體(不設上限,服務也不會平白多吃),而是為了三件事:
1. 讓失敗有邊界。 沒有上限的時候,一個服務出問題就吃光整台機器的記憶體,所有服務一起死,連 log 都來不及看。有了上限,出問題的那個被單獨殺掉,其他還活著,你有時間查。
2. 誠實面對「這個服務到底要多少」。 設上限的過程會逼你去實測。量過之後,你才知道推論引擎的峰值在哪、向量庫其實有多省。這些數字在之後的容量規劃、選機器規格、估雲端成本時都用得到。
3. 替 K8s 鋪路。 這是後來才體會到的。在 K8s 裡,requests、limits 都不填,Pod 會被歸成 BestEffort,節點資源一緊就最先被清掉;scheduler 也要靠 requests 決定 Pod 放哪個節點。在 compose 階段量出來的這組數字,搬到 K8s 時可以直接用,不用重新摸。
Day 03 提過一個差異,這裡再講一次,因為它很關鍵:
compose 的 mem_limit 基本上只有上限(mem_reservation 是軟性、沒有排程語意)。K8s 則分成 requests(scheduler 拿來排程的需求量)跟 limits(用量上限,記憶體超過就殺)兩個數字。
以我的推論引擎為例:requests 可以設在「日常穩態」附近(例如 2.5 GiB),limits 設在「峰值加 buffer」(6 GiB)。這樣 scheduler 用平時需要的 2.5 GiB 來排節點,同時允許它在載入模型時短暫衝到 6 GiB。
compose 沒有這種有排程意義的區分:實際上只有一個上限數字,設保守就浪費,設寬鬆又沒保障。這是我覺得 K8s 的資源模型比 compose 成熟的地方。
明天講 GPU:那是一個記憶體完全不能用「留 buffer」思維的世界。
設上限不是為了省資源,而是為了讓失敗有邊界,並且逼自己誠實量出每個服務真正需要多少。